
class Event

  @@subscriptions ||= {}
  @@published_events ||= {}

  attr_accessor :name, :data, :response

  def initialize name = nil, data = {}
    if !data.is_a? Hash
      raise "Second argument must be a Hash! was #{data.inspect}."
    end
    
    if name
      @name = name.to_s
    end

    @data = {}
    data.keys.each { |k|
      @data[k.to_s] = data[k]
    }
  end

  def method_missing *a
    key = a.first.to_s
    if @data.has_key? key
      @data[key]
    else
      super
    end
  end

  def to_json
    to_hash.to_json
  end

  def stop
    @stopped = true
    self
  end

  def stopped?
    @stopped || false
  end

  def to_hash
    @data['name'] = @name
    @data
  end

  def respond ev_name, ev_data = nil
    if ev_data == nil && ev_name.is_a?(Hash)
      ev_data = ev_name
      ev_name = @name+"_response"
    end
    if ev_name.is_a? Event
      e = ev_name
    else
      e = Event.new(ev_name, ev_data || {})
    end
    (@response ||=[]) << e
  end

  def == e
    e.class == self.class &&
      e.name == self.name &&
      e.data == self.data
  end

  def fire
    notify listeners
  end

  def notify listeners
    listeners.each { |listener|
      listener.call(self)
      break if stopped?
    }
    response
  end

  def listeners
    self.class.listeners self.class, name
  end

  def response
    @response || []
  end

  

  # static ---------------------------------------------------------------------

  def self.detach global_event, &block
    self.unsubscribe self, global_event, &block
  end

  def self.detach_all
    @@subscriptions[self] = {}
  end

  def self.publish who, event
    @@published_events[self] ||= {}
    @@published_events[self][who] ||= {}
    @@published_events[self][who][event] = true
  end

  def self.enable what
    what.class_eval('def on event_name, &block
      '+self.to_s+'.subscribe(self, event_name, &block)
    end')

    what.class_eval('def fire event_name, data = {}
      '+self.to_s+'.notify_listeners(self, event_name, data)
    end')
  end

  def self.list publisher
    @@published_events[self] or return []
    @@published_events[self][publisher].keys
  end

  def self.fire global_event, data = {}
    self.notify_listeners(self, global_event, data)
  end
  
  def self.notify_listeners sender, event_name, data = {}
    Event.new(event_name, data).notify(listeners sender, event_name)
  end
  
  def self.listeners subject, event = nil
    if !event
      event = subject
      subject = self
    else
      event = event.to_s
    end
    @@subscriptions ||= {}
    @@subscriptions[self] or return []
    @@subscriptions[self][subject.object_id] or return []
    @@subscriptions[self][subject.object_id][event] or return []
  end

  def self.on global_event, method = nil, &block
    if method.is_a? Array
      callback = method
      method = lambda { |e|
        callback[0].send(callback[1], e)
      }
    end
    callback = method || block
    self.subscribe self, global_event, callback
  end

  def self.subscribe subject, event, block = nil, &proc
    block ||= proc
    if !block
      raise 'No callback given.'
    end
    event = event.to_s
    check_allowed subject, event
    @@subscriptions[self] ||= {}
    @@subscriptions[self][subject.object_id] ||= {}
    @@subscriptions[self][subject.object_id][event] ||= []
    unless @@subscriptions[self][subject.object_id][event].include? block
      @@subscriptions[self][subject.object_id][event] << block
    end
  end

  def self.unsubscribe subject, event, &block
    event = event.to_s
    check_allowed subject, event
    @@subscriptions || return
    @@subscriptions[self] || return
    @@subscriptions[self][subject.object_id]  || return
    @@subscriptions[self][subject.object_id][event]  || return
    @@subscriptions[self][subject.object_id][event] = @@subscriptions[self][subject.object_id][event] - [block]
  end

  def self.check_allowed publisher, event
    @@published_events[self] ||= {}
    if (@@published_events[self][publisher.class]) && !(@@published_events[self][publisher.class].include? event)
      raise ArgumentError.new "The publisher doesn\'t know event '#{event}'."
    end
  end

  

  class TestEvent < Event; end


  class EventProvidingTest < FwTest
    class Event < TestEvent; end

    class EventEnabledRepo
      Event.enable(self)
      def persist object
        fire(:persist, {:object => object})
      end
      def event_with_param param
        fire(:event_with_param, {:param => param})
      end
    end

    class EventPublisher
      Event.enable(self)
      Event.publish(self, 'event_that_was_published')

      def event_that_was_published
        fire('event_that_was_published')
      end
    end

    def setup
      Event.detach_all
    end

    def test_listeners_subscribtion
      r = EventEnabledRepo.new
      callback = lambda {}
      r.on('persist', &callback)
      assert_equal([callback], Event.listeners(r, 'persist'))
    end

    def test_listeners_subscribe_directly_with_instance
      c1 = 0
      callback1 = lambda { c1 += 1 }
      r1 = EventEnabledRepo.new
      r1.on('persist', &callback1)

      c2 = 0
      callback2 = lambda { c2 += 1 }
      r2 = EventEnabledRepo.new
      r2.on('persist', &callback2)

      r1.fire('persist')
      r2.fire('persist')

      assert_equal c1, 1
      assert_equal c2, 1
    end


    def test_event_fire_calls_listeners
      r = EventEnabledRepo.new
      called = false
      r.on('persist') {
        called = true
      }
      r.persist "abcd"
      assert called
    end

    def test_events_can_use_symbol_too
      r = EventEnabledRepo.new
      called = false
      r.on(:persist) { called = true }
      r.persist "abcd"
      assert called
    end

    def test_event_with_parameter_passes_param_to_listeners
      r = EventEnabledRepo.new
      event = nil
      r.on('event_with_param') { |param|
        event = param
      }
      r.event_with_param "abcd"
      assert_equal event.param, "abcd"
    end

    def test_published_event_can_be_listed
      publisher = EventPublisher.new
      assert_equal ['event_that_was_published'], Event.list(publisher.class)
    end

    def test_listeners_can_be_added_to_published_Event_when_used_publish
      publisher = EventPublisher.new
      called = false
      publisher.on('event_that_was_published') {
        called = true
      }
      publisher.event_that_was_published
      assert called
    end

    def test_listening_to_nonpublished_event_causes_argument_error
      assert_raise(ArgumentError) {
        EventPublisher.new.on('event_that_was_NOT_published') {}
      }
    end
  end




  class TestGlobalEvents < FwTest
    class Event < TestEvent; end

    def setup
      Event.detach_all
    end

    def test_global_Event
      called = false
      Event.on('source:event') {
        called = true
      }
      assert !called
      Event.fire('source:event')
      assert called
    end

    def test_its_ok_to_fire_event_with_no_listeners
      okay = false
      begin
        Event.fire('blablasource:event')
        okay = true
      rescue
        fail "No raise expected, but was #{$!}"
      end
      assert okay
    end

    def test_subscribe_with_same_listener_only_once
      n = 0
      callback = lambda { n+=1 }
      Event.on('test:increment', &callback)
      Event.on('test:increment', &callback)
      Event.fire('test:increment')
      assert_equal 1, n
    end

    def test_detached_listener_does_not_get_called
      n = 0
      listener = lambda { n+=1 }
      Event.on('test:increment2', &listener)
      Event.detach('test:increment2', &listener)
      Event.fire('test:increment2')
      assert_equal 0, n
    end

    def test_event_response_on_event
      response = { 'ok' => true }
      Event.on('event_with_response') { |e|
          e.respond(response)
        }
      assert_equal Event.fire('event_with_response')[0].data, response
    end

    def test_detach_all
      called = 0
      Event.on('test:event') { called += 1 }
      Event.detach_all
      Event.fire('test:event')
      assert_equal 0, called
    end


    class DifferentEvent < Event; end

    def test_different_events_have_separate_subscriptions
      called = 0
      Event.on('test:event') { called += 1 }
      DifferentEvent.fire('test:event')
      assert_equal 0, called
      Event.fire('test:event')
      assert_equal 1, called
    end

    def test_different_events_have_separate_subscriptions
      called = 0
      Event.on('test:event') { called += 1 }
      DifferentEvent.detach_all
      Event.fire('test:event')
      assert_equal 1, called
    end
  end


  class Test < FwTest
    class InstanceTest < TestEvent; end

    def setup
      Event.detach_all
    end

    def test_data_keys_are_reachable_through_event_object
      ev = Event.new('event', {:abc => 123})
      assert_equal ev.abc, 123
      ev = Event.new('event', {'abc' => 123})
      assert_equal ev.abc, 123
    end

    def test_nil_data_keys_are_reachable_through_magic_methods
      ev = Event.new('event', {'abc' => nil})
      assert_equal ev.abc, nil
    end

    def test_response_can_omit_first_parameter_and_use_the_same_name
      e = Event.new('list_colors', {})

      colors = [:red, :green, :blue]
      e.respond({:colors => colors})

      assert_equal e.response[0].name, 'list_colors_response'
      assert_equal e.response[0].colors, colors
    end

    def test_response_takes_an_event
      e = Event.new('')
      e.respond(response1 = Event.new(nil))
      assert_equal e.response[0], response1
    end

    def test_event_converts_its_name_to_string
      e = Event.new(:go_home, {})
      assert_equal e.name, 'go_home'
    end

    def test_response_can_omit_second_parameter
      e = Event.new('go_home', {})
      e.respond(:already_home)
      assert_equal e.response[0].name, 'already_home'
    end

    def test_fire_self
      called = false
      Event.on('event') {
        called = true
      }
      assert !called
      Event.new('event').fire
      assert called
    end

    def test_events_with_same_names_and_params_are_equal
      assert_equal Event.new('event_name', { :a => 555}),
        Event.new('event_name', { :a => 555})
    end

    def test_subscribing_with_no_callback_should_raise_error
      assert_raise(RuntimeError) {
        Event.on('event') # no callback given
      }
    end
  end


  class TestBubbling < FwTest
    def test_newly_fired_event_is_not_stopped
      assert_equal Event.new.stopped?, false
    end

    def test_stopped_event_is_stopped
      assert_equal Event.new.stop().stopped?, true
    end

    def test_stopped_is_not_propagated_further
      called = []
      Event.on('test_stopping') { |e|
        called << 'one'
        e.stop
      }
      Event.on('test_stopping') { |e|
        called << 'two'
      }
      Event.fire('test_stopping')
      assert_equal called, ['one']
    end
  end


end

